查看原文
其他

CVE-2019-0708 bluekeep 漏洞研究分析详细完整版

MSA_Li 看雪学院 2021-03-07

本文为看雪论坛精华文章

看雪论坛作者ID:MSA_Li



一、概括


 
这个漏洞是微软在2019年被披露的漏洞中安全威胁最高的一个,在写本文之前,网上已经存在可以直接使用的poc,但质量参差不齐,而微软也及时修补了该漏洞。

这个漏洞主要涉及微软win7(x64/x86/server)和winXP(x64/x86)的远程桌面服务,漏洞分类为UAF。

远程桌面连接过程中包含几个协议,RDP,T.124,T.125,COTP,TPKT。这些协议运行在TCP协议上,所用端口为3389。



二、环境配置


 
测试环境为三台虚拟机,一台系统为win7(x64)作为远程服务的服务端,一台系统为win7(x86)作为客户端,一台kali linux(x64)作为辅助分析系统,两个win7虚拟机均未打补丁,所以都存在所述漏洞。


三、开始实验


 
实验的思路是从逆推法开始,即观察微软推送补丁所修改了哪些二进制文件,继而从中找出漏洞存在的相关逻辑和具体位置。


>>>>

对比二进制补丁


使用BinDiff软件观察修补前后的区别发现只有一个内核模块有改动,名为TermDD.sys。在进一步筛选发现除了_IcaBindVirtualChannels函数和_IcaRebindVirtualChannels函数以外,其他的改动都无关痛痒。根据这两个函数名字来看,我决定首先分析IcaBindVirtualChannels函数,我认为程序一般是先绑定然后再重新绑定(bind绑定)。此函数关键改动部分如下图(图1为补丁后,图2为补丁前)

图1:

图2:


看到其中更改了调用_IcaBindChannel的方式,如果比较的字符串等于“MS_T120” 则此调用的第三个参数设置为31。由于只有v18为false时才会执行补丁后的逻辑,所以我认为要触发该漏洞,则v17+268应该为“MS_T120”字符串,于是向前查找v17+268的线索,在返回v16的IcaFindChannelByName函数内部找到了答案。

图3:


可以看到参数a3应为要寻找通道的名称字符串,在循环中遍历对比v9+268,而返回值v9可以认为是目标通道的结构,而v9偏移268是通道结构中的通道名称。

上图中判断如果v9+268等于v3则返回v9。

通过以上分析,得出结论“MS_T120”是通道的名称,下一步就要找到如何调用这个函数,和参数的名称如何设置为“MS_T120”。

之后我在调用IcaFindChannelByName函数的地方下了一个断点,然后用正常的RDP客户端进行连接,当每次触发断点的时候检查下调用堆栈和通道的名称。

图4:


图5:


图5是第一次命中断点时,通道名称参数为“MS_T120”。后续的通道名称为CTXTW,rdpdr,rdpsnd,drdynvc,cliprdr。

然而根据图1的逻辑可知,仅当FindChannelByName函数调用成功,即通道已存在的情况下才会命中存在漏洞的代码段,命中后会创建“MS_T120“通道,所以要触发此漏洞,需要使用“MS_T120“作为通道名称再次调用IcaBindVirtualChannels。

回顾图4的调用堆栈可知最上层的调用是类似AcceptConnection的函数,根据名称猜测应该是在接受连接的时候创建通道,所以现在的目标就是寻找一个方法在连接后打开“MS_T120“通道。下一步准备用wireshark抓取RDP连接包进行分析,看看能不能找到一些线索。

>>>>

RDP抓包分析


win7系统默认远程桌面连接是用tls协议加密的,所以有以下两种方法解决,经过测试皆可达到目的:

1. (麻烦)直接抓取加密后的报文,再解密,方法如下

整体思路是客户端运行wireshark抓取加密的报文,再用名为mimikatz的开源工具导出RDP服务端的RSA私钥.pxf文件,再用openssl转成wireshark可识别的.pem文件格式。

但由于windowsXP及以上的系统内tls协议默认为双方协商加密算法和密钥交换算法,默认的加密算法是RSA-AES,没有问题,但默认的密钥交换算法是ECDHE椭圆曲线。

基于此交换算法即使得到RSA私钥也无法解密出用于加密传输通讯内容的AES对称密钥(不了解椭圆曲线可以自行百度),所以还要win+R打开gpedit.msc组策略编辑器将ssl密码套件设置为不带DHE的弱交换算法,如图6,再重新抓包后再按如上步骤将.pem导入wireshark即可。

图6:


2. (简单)修改系统设置为不使用ssl加密

在组策略编辑器中如图7所示将指定安全层改为RDP即可。

图7:


简单设置完TPKT协议3389端口后,发现第二个RDP报文如下:

图8:


此数据包包含传递给IcaBindVirtualChannels的六个通道中的四个,缺少“MS_T120”和CTXTW。通道在报文的顺序和在程序中打开的顺序一致。

由于报文中并未出现MS_T120和CTXTW通道,但程序中在其余通道前打开了它们,所以这两个通道应该是内部使用的通道,默认自动打开。

基于上述原因,想测试一下如果自己实现RDP协议,手动将MS_T120添加到通道数组中会不会像其他通道一样在程序中命中IcaBindVirtualChannels函数内的断点。

实现RDP协议需要用到kali中的metasploit模块,检查发现其中已经包含有RDP协议的完整Ruby模块,路径为/usr/share/Metasploit-framework/lib/msf/core/exploit/rdp.rb。在其中找到发送通道数组相关信息如图:

图9:


可以看到在构造RDP连接信息的函数中已经包含了“MS_T120“通道,于是仿照现有的RDP测试模块, 创建简单ruby测试模块,然后打开RDP服务端wireshark抓包,同时kali 用msf装载测试模块测试RDP服务端。

图10:


可见带有MS_T120的报文已经接到,然后我将断点移动到只有FindChannelByName成功的情况下 才命中的代码后程序成功执行到了具有漏洞部分的代码段。

目前从这个函数已经得不到更多线索了,想到既然MS_T120通道是内部使用和创建的,于是便寻找下创建的源头,在termdd模块内找到了IcaCreateChannel,创建通道的函数,在此函数上下断点,在进行RDP连接。断下后调用堆栈如下图:

图11:


从上图可以看到应用层ntdll_NtCreateFile打开设备符号连接,在往下查看IcaChannelOpen 函数内容并无线索,从名字观察猜测其下的MCSCreateDomain也许有创建内部通道的逻辑。如图:

图12:


果然在其中发现了以硬编码的方式用MS_T120调用IcaChannelOpen,,再深入函数内部发现第四个参数v5+36是用ZwCreateFile打开的文件句柄,这里猜想v5应该是通道结构,然后程序调用CreateIoCompletionPort创建此通道的异步IO端口,用于异步IO。

而且第二个CompletionPort参数不为NULL,查文档得知会有一个处理端口IO的函数,于是对此端口句柄查找交叉引用如图:

图13:


看名称先从MCSInitialize类似初始化的函数入手如图:

图14:

可以看到创建完端口后用端口句柄做参数创建了新的线程,进入新线程如图:

图15:


GetQueuedCompletionStatus函数检查发送到IO端口的数据,若成功接到数据则发给MCSPortData。

MCSPortData处理数据函数如下图:

注:win7 64位没有MCSPortData函数,此函数被就地内联,但代码逻辑和非内联版本一样,故不做区分。

图16:


可知缓冲区偏移30的数据如果是2则会关闭通道,而ReadFile的第二个参数证明了缓冲区的数据起始地址为偏移29。为了验证这个想法,编写了一个简易的msf模块,具备在打开的通道上发送信息的功能,测试了发送“lichao”,抓包截图如下:

图17:


windbg截图如下:

图18“:


由此可以证明,我们可以读写MS_T120通道,鉴于此漏洞为UAF类型,所以尝试发送满足关闭通道的数据看看有什么效果,即上文所述缓冲区偏移30为2,如图:

图19:


出现此错误的原因看似并不是二次释放,因为msf模块中默认带有额外填充式发送数据包,从而导致释放的结构中填充了随机数据作为代码运行,而此代码运行不符合内核IRQL级别。

根本原因还是在于系统试图关闭我已经关闭了的通道MS_T120,导致二次释放。

观察崩溃转储的调用堆栈信息可以发现驱动层有一个函数名为SignalBrokenConnection,属于RDPWD.sys模块的一员,如图:

图20:


IcaChannelInput函数的意思是找到一个指定ID对应的通道,第三个参数31即为通道绑定的ID,这与图1中加了补丁后的绑定MS_T120通道ID强制为31的代码相对应。

>>>>

找到漏洞利用关键点


分析到这里,漏洞的成因就比较清晰了,由于内部通道MS_T120第一次由系统绑定ID为31,而后在易受攻击的代码中被再次绑定到另一个ID,导致一个MS_T120通道结构存在两个引用,而且可以通过引用控制通道数据,随后通过第二次绑定的引用释放掉MS_T120通道结构,最后断开连接时系统将再次释放此结构,达成Double-Free。

下面就要寻找一些在系统内部释放通道时对此通道数据有存在关联的代码,对于UAF类型的漏洞利用大致就是释放掉MS_T120通道,然后在原来的位置上分配一个假对象,最后系统自动释放时调用伪造的对象数据完成利用。

由创建通道的代码逻辑可知通道数据结构在内核非分页内存中分配,所以就要寻找在非分页内存中写任意数据的方法,随后我们在造成崩溃的二次free中查看调用堆栈,发现IcaChannelInputInternal函数中有调用通道结构中的函数,如图:

图21:


可以看到通道结构体偏移256字节是一个二重函数指针,那么就可以从v21这里劫持控制流,然后通过追踪上次发送通道消息的调用堆栈发现IcaChannelInputInternal函数在处理消息数据的代码中有些线索,如图:

图22:


图23:


图22的所有代码均在一个while(1)循环中,图23在while循环外面,根据逻辑可知只有命中最开始的break跳出循环才会执行图23分配内存,否则无论如何都会执行IoCompleteRequest结束IRP。

返回观察图21、22、23感觉图21中v21函数疑似数据处理函数,此函数的参数都与之后分配非分页内存的大小v32,memmove的大小v6和拷贝地址v7相关,v7应该是数据指针,而且根据图23可知实际分配的内存大小v32比数据大小多56字节。

这样我们已经找到在满足break条件下可以在非分页内存分配任意大小并写入任意数据的方法,现在返回观察需要二次利用的通道结构内存是如何分配的,如图:

图24:


可知每个通道结构固定分配352字节,现在的思路已经很清晰了,主动绑定MS_T120通道为非31的ID,然后发送数据包释放MS_T120通道,然后利用其他通道发送数据达到任意地址写任意数据的堆喷射占据释放的MS_T120结构,最后正常断开连接系统调用伪造的数据执行内核shellcode。

还有一些关键点,比如win7中内核内存没有DEP数据执行保护,非分页内存启始地址为
nt!MnNonPagedPoolStart,定位shellcode比较简单。


分析exploit模块


 
直接查看msf的bluekeep模块发现作者zerosum0x0的实现选择了RDPSND信道作为辅助发送数据通道,此通道恰好可以满足图22中break的条件,顺利分配内存。

RDPSND属于音频播放功能,在win7上默认开启,server 2008 默认关闭,可以通过注册表或远程桌面配置进行修改,前文提到分配的非分页内存会比实际发送的数据多56字节,所以经过计算需要发送296字节数据,这样才会分配352字节内存,与MS_T120通道大小相同。

而在设置伪造通道结构的函数指针指向shellcode时尽量选取一个较大的地址,这样的地址内存利用率较低,大概率写入shellcode,而非分页内存启始地址在不同环境中略有区别,所以msf模块针对不同场景设置了不同的选项,选项还有为虚拟机环境设置的地址,主要由于虚拟机内存设置的可交换选项导致子系统会在非分页内存前分配大量PTE页表,导致NPP地址变化。 如图:

图25:



另外一个问题是,在这种情况下命中的shellcode所在模块是termdd.sys属于内核模块,IRQL级别为DISPATCH_LEVEL,如果不做处理则shellcode只能执行相当受限的功能,所以需要方法完成R0到R3的转换,这一点和之前著名的‘永恒之蓝’的做法大同小异。

msf现有的payload思路是准备两段shellcode,一段用户态,一段内核态,分两段的原因在于对通道一次发送的数据有最大限制为0x400 – 0x48 = 0x3B8 字节,其中0x48为固定大小信息头,因为不足以容纳所有的shellcode,所以作者采用两段shellcode交叉布局的方式进行堆喷,如:内核shellcode+用户态shellcode+内核shellcode+用户态shellcode 。

其中每段分配的内存都是0x400,然后利用内核态shellcode执行APC注入spoolsrv.exe进程,并拷贝R3 shellcode到目标进程空间,完成R0到R3的转换。

用户态payload分为两个部分,第一个部分是通过“egg hunter”(根据代码特征寻找)的方式找到内核态payload,第二部分是要作为APC执行的用户态payload,如图:

图26:


egg_loop是替换过变量值的目的寻找内核态shellcode,上下两个EGG是用户态shellcode的寻找标志用作分隔,最后是被注入进程要执行的R3 payload。(默认是弹 system权限的 shell连接,是MSF众多可选payload之一)

而内核态shellcode就比较长了,涉及到诸多功能,这也解释了为什么0x400的通道空间放不下的原因,主要是内核shellcode + R3 payload比较大,在开始部分作者硬编码了一些需要用到的函数和结构的hash和偏移,由于可选的测试环境都是win7/2008,所以这些值都是通用的。如图:

图27:


开头的几个offset是当R0 shellcode执行时将获取的相关数据保存到相对内核最高段HAL地址的偏移,即上图的hal_heap_storage,下面是shellcode内容,如图:

图28:


首先调用sti允许中断发生,然后进入主逻辑,结束后选择直接退出IcaChannelInputInternal函数,不再执行之后的代码,避免麻烦,因为之后的代码逻辑依赖于正常的数据和调用,而我们劫持调用后的数据是错的。

但也造成一个小问题,此函数之前获得的一个锁因为跳过没有被释放,不过就多次测试结果来看,并没有什么影响,而这段退出的代码和原函数结尾是一样的,另外可以发现这两段shellcode头部并没有大量nop指令做滑板,因为内核非分页内存布局对齐比较规律,堆喷之后两段shellcode首部大概率命中经过计算的整数地址,测试验证成功率很高。

下面进入r3_to_r0_start主逻辑,概括起来分为几个步骤:

1. 找到ntoskrnl内核模块基址。



2. 获取当前的EPROCESS和ETHREAD结构。



3. 获取EPROCESS结构中ImageFilename的偏移。



4. 获取EPROCESS结构中ThreadListHead的偏移。



5. 获取ETHREAD结构中ThreadListEntry的偏移。



6. 获取EPROCESS结构中ActiveProcessLinks(活动进程链表)的偏移。



7. 遍历ActiveProcessLinks找到要注入的spoolsv.exe进程(EPROCESS)。





8. 调用PsGetProcessPeb获取spoolsv.exe的PEB结构。



9. 遍历spoolsv.exe的ThreadList执行KeInsertQueueApc直到成功

当APC执行时是在spoolsv.exe进程上下文,这里插入了两个APC,一个R0 APC,一个R3 APC,R3 APC 函数的地址目前是无效的,待后续步骤中再改,R0 APC 先被调用。(代码比较长,只截图了关键部分)



上文中获取的所有的数据都保存在内核最高段的HAL地址中(非分页且自系统启动以来就没有用到)。插入的R0 APC执行逻辑如下:

1. 通过之前保存的nt kernel模块地址找到它的ZwAllocateVirtualMemory函数。



2. 调用ZwAllocateVirtualMemory分配R3可执行内存并将上图第二个参数R3 APC函数的地址改为刚分 配的shellcode地址。



3. 定位shellcode并复制到刚分配的内存中。



4. 通过之前保存的spoolsv.exe的PEB遍历加载模块获得kernel32.dll地址。

5. 遍历kernel32.dll的导出表找到CreateThread函数地址。

6. 将CreateThread函数地址保存到参数SystemArgument1中,此参数会被系统传给R3 APC。



另一个插入的R3 APC即为要执行的payload,开头部分利用参数。

CreateThread函数创建子线程执行若干字节后的真正的payload功能代码,父线程创建完线程后ret返回结束APC,至此,整个shellcode执行流程结束。


五、总结


 
整个过程用逆推法开始分析,逐步找到漏洞点,在分析过程中有一个小插曲,MSF这个利用模块的作者更新了他的shellcode代码,不再使用hook syscall的方式,转而通过各种数据结构信息完成利用APC,猜测原因可能是这种方式比hook syscall更加稳定,破坏性的更改较少。

纵观shellcode中也提取了常见的功能且频繁调用,比如计算函数名hash、根据模块查函数地址。

经过测试发现对于目前的exploit,目标系统内存必须2G或以上才能成功,如果想要达到稳定的效果可以根据不同内存大小修改exploit选项中的GROOMBASE,参考图25。

写本文时参考了一些网上的文章,发现不论是分析漏洞的过程还是分析shellcode过程都比较简略,踩坑无数,真可谓曲折前进。

 最后放一张成功弹shell的截图:







- End -






看雪ID:MSA_Li

https://bbs.pediy.com/user-858669.htm 

*本文由看雪论坛  MSA_Li  原创,转载请注明来自看雪社区




推荐文章++++

新手熊猫烧香学习笔记

GandCrab v5.2 分析

定制Xposed框架

Android 4.4 从url.openConnection到DNS解析

通过一道pwn题详细分析retdlresolve技术


进阶安全圈,不得不读的一本书







公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com


“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存